/*
 * Copyright 2022-2023 the original author(https://github.com/wj596)
 * 
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */
package org.jsets.fastboot.flowable.service.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.SequenceFlow;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.common.engine.impl.util.io.InputStreamSource;
import org.flowable.common.engine.impl.util.io.StreamSource;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.jsets.fastboot.flowable.component.CustomProcessDiagramGenerator;
import org.jsets.fastboot.flowable.component.DingModelConvertor;
import org.jsets.fastboot.flowable.config.FlowableConfig;
import org.jsets.fastboot.flowable.mapper.DeModelMapper;
import org.jsets.fastboot.flowable.mapper.CompositeModelMapper;
import org.jsets.fastboot.flowable.model.dto.FlowModelQuery;
import org.jsets.fastboot.flowable.model.dto.FlowModelSettingParam;
import org.jsets.fastboot.flowable.model.dto.ProcessNode;
import org.jsets.fastboot.flowable.model.entity.DeModel;
import org.jsets.fastboot.flowable.model.entity.CompositeModel;
import org.jsets.fastboot.flowable.service.ICompositeElementService;
import org.jsets.fastboot.flowable.service.ICompositeModelService;
import org.jsets.fastboot.persistent.support.LambdaQuerier;
import org.jsets.fastboot.util.IoUtils;
import org.jsets.fastboot.util.JsonUtils;
import org.jsets.fastboot.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.flowable.editor.constants.ModelDataJsonConstants;
import org.flowable.editor.language.json.converter.BpmnJsonConverter;
import org.flowable.editor.language.json.converter.util.CollectionUtils;

/**
 * @author wangjie (https://github.com/wj596)
 * @date 2021.06.25 11:23
 */
@Slf4j
@Service
public class CompositeModelService extends ServiceImpl<CompositeModelMapper, CompositeModel> implements ICompositeModelService {

	@Autowired
	private FlowEngineService flowEngine;
	@Autowired
	private ObjectMapper objectMapper;
	@Autowired
	private DeModelMapper deModelMapper;
	@Autowired
	private ICompositeElementService compositeElementService;
	@Autowired
	private DingModelConvertor convertor;

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void importBpmnModelFile(String filename, InputStream inputStream) {
		String fileType = filename.substring(filename.indexOf("."));
		if (FlowableConfig.ZIP_FILE_SUFFIX.equals(fileType)) {
			int imported = 0;
			try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
				ZipEntry entry;
				while ((entry = zipInputStream.getNextEntry()) != null) {
					String suffix = entry.getName().substring(entry.getName().indexOf("."));
					if (!(FlowableConfig.PROCESS_DEF_SUFFIX_BPMN.equalsIgnoreCase(suffix)
							|| FlowableConfig.PROCESS_DEF_SUFFIX.equalsIgnoreCase(suffix))) {
						continue;
					}
					imported++;
					int byt;
					List<Byte> byteList = new ArrayList<>();
					while ((byt = zipInputStream.read()) != -1) {
						byteList.add((byte) byt);
					}
					Byte[] tempArr = new Byte[byteList.size()];
					byteList.toArray(tempArr);
					byte[] bytes = ArrayUtils.toPrimitive(tempArr);
					this.importBpmnModel(entry.getName(), bytes);
					zipInputStream.closeEntry();
				}
			} catch (IOException e) {
				log.error(e.getMessage(), e);
				throw new RuntimeException(e);
			}
			if (0 == imported) {
				throw new RuntimeException("Zip包中没有流程模型文件");
			}
		} else {
			if (!(FlowableConfig.PROCESS_DEF_SUFFIX_BPMN.equalsIgnoreCase(fileType)
					|| FlowableConfig.PROCESS_DEF_SUFFIX.equalsIgnoreCase(fileType))) {
				throw new RuntimeException("不是有效的流程模型文件");
			}
			try {
				byte[] bytes = new byte[inputStream.available()];
				inputStream.read(bytes);
				this.importBpmnModel(filename, bytes);
			} catch (IOException e) {
				log.error(e.getMessage(), e);
				throw new RuntimeException(e);
			} finally {
				IoUtils.closeQuietly(inputStream);
			}
		}
	}

	@Override
	public void saveDingModel(CompositeModel flowModel) {
		BpmnModel bpmnModel = this.convertor.convert(flowModel, true);

		RepositoryService repositoryService = this.flowEngine.getRepositoryService();
		String modelKey = flowModel.getModelKey();
		String modelName = flowModel.getName();
		int modelVersion = 1;
		long modelCount = repositoryService.createModelQuery().modelKey(modelKey).count();
		if (modelCount > 0) {
			modelVersion = (int) (modelCount + 1);
		}

		Model model = this.buildModel(modelKey, modelName, modelName, modelVersion);
		bpmnModel.setTargetNamespace(model.getCategory());
		Deployment deploy = repositoryService.createDeployment()
				.key(bpmnModel.getMainProcess().getId())
				.name(bpmnModel.getMainProcess().getName())
				.category(model.getCategory())
				.addBpmnModel(modelKey + ".bpmn", bpmnModel)
				.deploy();
		model.setDeploymentId(deploy.getId());
		this.saveBpmnModel(bpmnModel, model);

		flowModel.setDingModel(true);
		flowModel.setActReModelId(model.getId());
		flowModel.setVersion(modelVersion);
		flowModel.setDeploymentId(deploy.getId());
		flowModel.setDeploymentTime(new Date());
		if (Objects.isNull(flowModel.getId())) {// 新增
			flowModel.setCreateTime(new Date());
			this.getBaseMapper().insert(flowModel);
		} else {
			this.getBaseMapper().updateById(flowModel);
		}
	}

	@Override
	@Transactional(rollbackFor = Exception.class)
	public void settingModel(FlowModelSettingParam param) {
		CompositeModel flowModel = this.getById(param.getModelId());
		if (null == flowModel) {
			return;
		}
		
		this.compositeElementService.deleteByModel(param.getModelId());
		if (CollectionUtils.isNotEmpty(param.getElements())) {
			this.compositeElementService.saveBatch(param.getElements());
		}
		
		flowModel.setFormId(param.getFormId());
		flowModel.setFirstNodeAutoComplete(param.getFirstNodeAutoComplete());
		flowModel.setStartedWebhook(param.getStartedWebhook());
		flowModel.setCompletedWebhook(param.getCompletedWebhook());
		this.updateById(flowModel);
	}

	@Override
	@Transactional
	public void deleteById(String id) {
		CompositeModel flowModel = this.getById(id);
		if (null != flowModel) {
			this.flowEngine.getRepositoryService().deleteDeployment(flowModel.getDeploymentId(), true);
			this.flowEngine.getRepositoryService().deleteModel(flowModel.getActReModelId());
			this.removeById(id);
		}
	}

	@Override
	public Page<CompositeModel> selectPage(FlowModelQuery criteria) {
		return new LambdaQuerier<>(CompositeModel.class).excludes(CompositeModel::getDingEditorJson)
				.setBaseMapper(this.getBaseMapper()).setCurrentPage(criteria.getCurrent())
				.setPageSize(criteria.getSize()).likeIfNecessary(CompositeModel::getModelKey, criteria.getKey())
				.likeIfNecessary(CompositeModel::getName, criteria.getName())
				.eqIfNecessary(CompositeModel::getVersion, criteria.getVersion()).selectPage();
	}

	@Override
	public Optional<CompositeModel> findByKey(String key) {
		return new LambdaQuerier<CompositeModel>()
				.setBaseMapper(this.getBaseMapper())
				.eq(CompositeModel::getModelKey, key)
				.findOne();
	}

	@Override
	public Optional<CompositeModel> findByDeploymentId(String deploymentId) {
		return new LambdaQuerier<CompositeModel>()
				.setBaseMapper(this.getBaseMapper())
				.eq(CompositeModel::getDeploymentId, deploymentId)
				.findOne();
	}

	@Override
	public InputStream getImageByDeploymentId(String deploymentId) {
		ProcessDefinition pdf = this.flowEngine.getRepositoryService().createProcessDefinitionQuery().deploymentId(deploymentId).singleResult();
		if (pdf == null) {
			throw new RuntimeException("查询不到流程定义实体");
		}
		InputStream is = this.flowEngine.getRepositoryService().getResourceAsStream(deploymentId, pdf.getDiagramResourceName());
		return is;
	}

	@Override
	public InputStream getHighlightImage(String processInstanceId) {
		String processDefinitionId = "";
		Set<String> currIds = Sets.newHashSet();
		ProcessInstance processInstance = this.flowEngine.getRuntimeService().createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
		if (processInstance == null) {
			HistoricProcessInstance historicProcessInstance = this.flowEngine.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
			processDefinitionId = historicProcessInstance.getProcessDefinitionId();
		} else {
			processDefinitionId = processInstance.getProcessDefinitionId();
			this.flowEngine.getRuntimeService().createExecutionQuery().processInstanceId(processInstance.getId()).list()
					.forEach(t -> {
						if (StringUtils.notEmpty(t.getActivityId())) {
							currIds.add(t.getActivityId());
						}
					});
		}

		List<String> highLightedActivities = new ArrayList<>();
		List<String> highLightedFlows = new ArrayList<>();
		List<HistoricActivityInstance> historicActivities = this.flowEngine.getHistoryService()
				.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId)
				.orderByHistoricActivityInstanceStartTime().asc().list();
		for (HistoricActivityInstance tempActivity : historicActivities) {
			String activityId = tempActivity.getActivityId();
			highLightedActivities.add(activityId);
			if (FlowableConfig.ACTIVITY_TYPE_SEQUENCE_FLOW.equals(tempActivity.getActivityType())) {
				highLightedFlows.add(tempActivity.getActivityId());
			}
		}

		SpringProcessEngineConfiguration engineConfig = this.flowEngine.getConfig();
		BpmnModel bpmnModel = this.flowEngine.getRepositoryService().getBpmnModel(processDefinitionId);
		CustomProcessDiagramGenerator diagramGenerator = (CustomProcessDiagramGenerator) engineConfig
				.getProcessDiagramGenerator();
		InputStream in = diagramGenerator.generateDiagram(bpmnModel, "bmp", highLightedActivities, highLightedFlows,
				engineConfig.getActivityFontName(), engineConfig.getLabelFontName(),
				engineConfig.getAnnotationFontName(), engineConfig.getClassLoader(), 1.0, true, currIds);
		return in;
	}

	@Override
	public List<ProcessNode> iterateFlowElements(String modelId) {
		try {
			Model model = this.flowEngine.getRepositoryService().createModelQuery().modelId(modelId).singleResult();
			byte[] source = this.flowEngine.getRepositoryService().getModelEditorSource(model.getId());
			JsonNode modelNode = this.objectMapper.readTree(source);
			BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(modelNode);
			org.flowable.bpmn.model.Process mainProcess = bpmnModel.getMainProcess();
			FlowNode startFlowNode= null; 
			Map<String, FlowNode> nodeMap = Maps.newHashMap();
			for (FlowElement fe : mainProcess.getFlowElements()) {
				if (fe instanceof FlowNode) {
					FlowNode node = (FlowNode) fe;
					if (null == startFlowNode && fe instanceof StartEvent) {
						startFlowNode = node;
					}
					nodeMap.put(fe.getId(), node);
				}
			}
			
			ProcessNode startProcessNode = this.buildProcessNode(startFlowNode, 1);
			Set<ProcessNode> result = Sets.newHashSet();
			result.add(startProcessNode);
			if (null != startProcessNode) {
				this.elementRecursive(result, nodeMap, startFlowNode, startProcessNode.getLevel());
			}
			List<ProcessNode> processNodes = result.stream()
					.filter(distinctBy(n -> n.getId()))
					.sorted((o1, o2) -> o1.getLevel() - o2.getLevel())
					.collect(Collectors.toList());
			return processNodes;
		} catch (IOException e) {
			log.error(e.getMessage(), e);
			throw new RuntimeException();
		}
	}

	private <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) { 
		final Set<Object> seen = ConcurrentHashMap.newKeySet(); 
		return t -> seen.add(keyExtractor.apply(t)); 
	}

	private void elementRecursive(Set<ProcessNode> result, Map<String, FlowNode> nodeMap, FlowNode currentNode, int currentLevel) {
		if (CollectionUtils.isEmpty(currentNode.getOutgoingFlows())) {
			return;
		}

		List<SequenceFlow> children = currentNode.getOutgoingFlows();
		for (SequenceFlow child : children) {
			FlowNode node = nodeMap.get(child.getTargetRef());
			if (null != node) {
				int nextLevel = currentLevel+1;
				result.add(this.buildProcessNode(node, nextLevel));
				elementRecursive(result, nodeMap, node, nextLevel);
			}
		}
	}

	/**
	 * 导入模型
	 * 
	 * @param fileName   模型文件名称
	 * @param bytes      模型数据
	 * @param needDeploy 是否需要发布
	 */
	/**
	 * @param fileName
	 * @param bytes
	 */
	private void importBpmnModel(String fileName, byte[] bytes) {
		StreamSource xmlSource = new InputStreamSource(new ByteArrayInputStream(bytes));
		BpmnModel bpmnModel = this.convertor.getBpmnXMLConverter().convertToBpmnModel(xmlSource, false, false, this.flowEngine.getConfig().getXmlEncoding());
		String modelName = bpmnModel.getMainProcess().getName();
		String modelKey = bpmnModel.getMainProcess().getId();
		int modelVersion = 1;
		long modelCount = this.flowEngine.getRepositoryService().createModelQuery().modelKey(modelKey).count();
		if (modelCount > 0) {
			modelVersion = (int) (modelCount + 1);
		}
		// 构造模型
		Model modelData = this.buildModel(modelKey, modelName, modelName, modelVersion);
		bpmnModel.setTargetNamespace(modelData.getCategory());
		Deployment deploy = this.flowEngine.getRepositoryService()
				.createDeployment()
				.key(bpmnModel.getMainProcess().getId())
				.name(bpmnModel.getMainProcess().getName())
				.category(modelData.getCategory())
				.addBpmnModel(fileName, bpmnModel)
				.deploy();
		String deployId = deploy.getId();
		modelData.setDeploymentId(deployId);
		this.saveBpmnModel(bpmnModel, modelData);
		log.info("[{}]模型文件导入部署成功,部署ID[{}]", fileName, deployId);
		boolean deUpdate = true;
		DeModel deModel = this.deModelMapper.selectByKey(modelKey);
		if (Objects.isNull(deModel)) {
			deModel = new DeModel();
			deModel.setId(StringUtils.getUUID());
			deModel.setModelKey(modelKey);
			deModel.setCreated(new Date());
			deModel.setCreatedBy("system");
			deUpdate = false;
		}
		deModel.setName(modelName);
		deModel.setLastUpdated(new Date());
		deModel.setLastUpdatedBy("system");
		deModel.setVersion(modelVersion);
		deModel.setModelEditorJson(this.convertor.getBpmnJsonConverter().convertToJson(bpmnModel).toString());
		deModel.setModelType(0);
		if (deUpdate) {
			this.deModelMapper.updateById(deModel);
		} else {
			this.deModelMapper.insert(deModel);
		}

		CompositeModel flowModel = null;
		Optional<CompositeModel> optional = this.findByKey(modelKey);
		if (optional.isPresent()) {
			flowModel = optional.get();
		} else {
			flowModel = new CompositeModel();
			flowModel.setModelKey(modelKey);
			flowModel.setDingModel(false);
			flowModel.setCreateTime(new Date());
		}
		flowModel.setActDeModelId(deModel.getId());
		flowModel.setActReModelId(modelData.getId());
		flowModel.setVersion(modelVersion);
		flowModel.setDeploymentId(deployId);
		flowModel.setDeploymentTime(new Date());
		flowModel.setName(modelName);
		if (optional.isPresent()) {
			this.updateById(flowModel);
		} else {
			this.save(flowModel);
		}
	}

	/**
	 * 保存BpmnModel
	 * 
	 * @param bpmnModel Bpmn模型
	 * @param model     Represents模型
	 */
	private void saveBpmnModel(BpmnModel bpmnModel, Model model) {
		String modelId = null;
		try {
			ObjectNode editorNode = this.convertor.getBpmnJsonConverter().convertToJson(bpmnModel);
			this.flowEngine.getRepositoryService().saveModel(model);
			modelId = model.getId();
			this.flowEngine.getRepositoryService().addModelEditorSource(modelId, editorNode.toString().getBytes(StandardCharsets.UTF_8));
		} catch (Throwable e) {
			if (modelId != null) {// Rollback
				this.flowEngine.getRepositoryService().deleteModel(modelId);
			}
			throw new RuntimeException(e);
		}
	}

	/**
	 * 构建模型
	 * 
	 * @param key          模型KEY
	 * @param name         模型名称
	 * @param desc         描述
	 * @param modelVersion 模型版本
	 * @return Model
	 */
	private Model buildModel(String key, String name, String desc, int modelVersion) {
		Model model = this.flowEngine.getRepositoryService().newModel();
		ObjectNode modelObjectNode = this.objectMapper.createObjectNode();
		modelObjectNode.put("key", key);
		modelObjectNode.put(ModelDataJsonConstants.MODEL_NAME, name);
		modelObjectNode.put(ModelDataJsonConstants.MODEL_REVISION, modelVersion);
		modelObjectNode.put(ModelDataJsonConstants.MODEL_DESCRIPTION, desc);
		model.setKey(key);
		model.setName(name);
		model.setVersion(modelVersion);
		model.setMetaInfo(modelObjectNode.toString());
		return model;
	}
	
	private ProcessNode buildProcessNode(FlowNode flowNode, int level) {
		ProcessNode pn = new ProcessNode();
		pn.setId(flowNode.getId());
		pn.setName(flowNode.getName());
		pn.setType(flowNode.getClass().getSimpleName());
		pn.setLevel(level);
		return pn;
	}

}